--
-- Author: shunguo.chen
-- Date: 2016-10-12 10:40:46
--

local table_insert = table.insert
local table_sort = table.sort

local string_find = string.find
local string_gsub = string.gsub
local string_lower = string.lower
local string_split = string.split
local string_trim = string.trim

local _ranges = {
	{0,59},
	{0,59},
	{0,23},
	{1,31},
	{1,12},
	{0,6}
}
local _attribute = {
    'sec',
    'min',
    'hour',
    'day',
    'month',
    'wday'
}
local _alternate = {}
_alternate[5] = {jan=1, feb=2, mar=3, apr=4, may=5, jun=6, jul=7, aug=8, sep=9, oct=10, nov=11, dec=12}
_alternate[6] = {sun=0, mon=1, tue=2, wed=3, thu=4, fri=5, sat=6}

local _daymax = {31,28,31,30,31,30,31,31,30,31,30,31}

local function getDayMax(month, year)
	if month == 2 then
		local isLeap = false
		if year % 100 == 0 then
			isLeap = (year % 400 == 0)
		else
			isLeap = (year % 4 == 0)
		end
		return isLeap and 29 or 28
	else
		return _daymax[month]
	end
end

local Matcher = class("Matcher")

function Matcher:ctor(idx, entry)
	if idx ~= 4 and idx ~= 6 and string_find(entry, "?") then
		assert(false, "cannot use '?' in the field = " .. _alternate[idx])
	end

    self.idx = idx
    self.input = string_lower(entry)

    --替换月份或星期的简写
    if self.idx == 5 or self.idx == 6 then
    	local repAry = _alternate[self.idx]
    	for k,v in pairs(repAry) do
    		self.input = string_gsub(self.input, k, v)
    	end
    end

    -- parse ","
    self.splits = string_split(self.input, ",")
    self.allows = {}

    -- parse "*" / "?"
    self.any = false
    for i,v in ipairs(self.splits) do
    	if v == "*" or v == "?" then
    		self.any = true
    		break
    	end
    end

    if not self.any then
	    for i,v in ipairs(self.splits) do
	    	self:_parse(v)
	    end
	    assert(#self.allows > 0, "crontab parse error entry = " .. string_lower(entry))
    	table_sort(self.allows, function(a, b)
			return a < b
	    end)
	end
end

function Matcher:_parse(entry)
	local increment = nil
	local min = _ranges[self.idx][1]
	local max = _ranges[self.idx][2]

	-- parse "/"
	if string_find(entry, "/") then
		local ary = string_split(entry, "/")
		increment = tonumber(ary[2])
		assert(increment and increment > 0, "sign '/' increment error, you provided = " .. entry)
		entry = ary[1]
	end

	-- parse "-"
	if string_find(entry, "-") then
		local ary = string_split(entry, "-")
		local min_ = tonumber(ary[1])
		local max_ = tonumber(ary[2])
		assert(min_ and max_ and min_ >= min and max_ <= max and min_ < max_, 
			"sign '-' out of range, entry = " .. entry)

		min = min_
		max = max_
		increment = increment or 1
	elseif entry == "*" then
		
	else
		local min_ = tonumber(entry)
		assert(min_ and min_ >= min and min_ <= max, 
			"not number or out of range, entry = " .. entry)
		min = min_
	end

	if increment then
		for i=min,max,increment do
			if i <= max then
				table_insert(self.allows, i)
			end
		end
	else
		table_insert(self.allows, min)
	end
end

-- 星期不适用
function Matcher:getMin()
	if self.any then
		return _ranges[self.idx][1]
	else
		return self.allows[1]
	end
end
-- 星期不适用，日期需传入month和year
function Matcher:getMax(month, year)
	if self.any then
		if self.idx ~= 4 then
			return _ranges[self.idx][2]
		else
			return getDayMax(month, year)
		end
	else
		return self.allows[#self.allows]
	end
end

-- 星期不适用
function Matcher:compare(date, isPre)
	isPre = isPre or false
	local now = date[_attribute[self.idx]]
	local isLend = false
	local ret = now

	local min = _ranges[self.idx][1]
	local max = _ranges[self.idx][2]
	if self.idx == 4 then
		max = getDayMax(date.month, date.year)
	end

	if self.any then
		if isPre then
			if now > max then
				assert(false, "Matcher:compare pre now > max")
			elseif now < min then
				isLend = true
				ret = max
			end
		else
			if now > max then
				isLend = true
				ret = min
			elseif now < min then
				assert(false, "Matcher:compare next now < min")
			end
		end
	else
		local valueMin = self:getMin()
		local valueMax = self:getMax()
		if isPre then
			if now < valueMin then
				isLend = true
				ret = valueMax
			else
				for i=#self.allows,1,-1 do
					local v = self.allows[i]
					if v <= now then
						ret = v
						break
					end
				end

				if ret < min then
					isLend = true
					ret = valueMax
				end
			end
		else
			if now > valueMax then
				isLend = true
				ret = valueMin
			else
				for i,v in ipairs(self.allows) do
					if v >= now then
						ret = v
						break
					end
				end

				-- fix 2月29日不是每年都有
				if ret > max then
					isLend = true
					ret = valueMin
				end
			end
		end
	end
	-- printf("Matcher:compare isPre = %s, isLend = %s, ret = %s", tostring(isPre), tostring(isLend), ret)
	return isLend, ret
end

function Matcher:getWdayGap(now, isPre)
	isPre = isPre or false
	-- print("now, isPre", now, isPre)
	if self.any then 
		return 0
	else
		if isPre then
			for i=#self.allows,1,-1 do
				local v = self.allows[i]
				if v <= now then
					return now - v
				end
			end
			return now - self.allows[#self.allows] + 7
		else
			for i,v in ipairs(self.allows) do
				if v >= now then
					return v - now
				end
			end
			return self.allows[1] - now + 7
		end
	end
end

----------------------------
local Crontab = class("Crontab")

function Crontab:ctor(crontab)
    self:_init(crontab)
end

function Crontab:setData(crontab)
	self:_init(crontab)
end

function Crontab:_init(crontab)
	assert(crontab, "crontab can't be nil")
	-- 去除首尾空白字符 多个空格替换成一个空格
	crontab = string_trim(crontab)
	crontab = string_gsub(crontab, "([ ]+)", " ")

	self.crontab = crontab
	self.matchers = {}
	local ary = string_split(crontab, " ")
	assert(#ary == 6, "crontab split error crontab=" .. crontab)
	for i,v in ipairs(ary) do
		self.matchers[_attribute[i]] = Matcher.new(i, v)
		-- dump(self.matchers[_attribute[i]], i)
	end

	self.isAnyDay = self.matchers[_attribute[4]].any
	self.isAnyWday = self.matchers[_attribute[6]].any
	if not self.isAnyDay and not self.isAnyWday then
		assert(false, "crontab day and wday can't all set")
	end
end

function Crontab:next(nowTime, isPre, isLast)
	assert(nowTime ~= nil, "Crontab:next nowTime can't be nil")

	isPre = isPre or false
	isLast = isLast or false
	local log = isPre and "Crontab:previous" or "Crontab:next"
	-- print(log .. " nowTime,crontab,isPre,isLast", nowTime, self.crontab, isPre, isLast)

	local refreshIdx = 0
	local factor = isPre and -1 or 1
	local date = os.date("*t", nowTime)
	-- dump(date, log .. " date start")
	
	-- sec min hour
	for i=1,3 do
		local key = _attribute[i]
		local matcher = self.matchers[key]
		local isLend, value = matcher:compare(date, isPre)
		if date[key] ~= value then
			refreshIdx = i - 1
		end
		date[key] = value
		if isLend then
			date[_attribute[i+1]] = date[_attribute[i+1]] + 1 * factor
		end
	end

	-- dump(date, log .. " date 1")

	if self.isAnyWday then
		while true do
			-- day
			local matcher = self.matchers[_attribute[4]]
			local isLend, value = matcher:compare(date, isPre)
			if value ~= date.day then
				refreshIdx = 3
			end
			date.day = value
			if isLend then
				date.month = date.month + 1 * factor
			end

			-- month
			local matcher = self.matchers[_attribute[5]]
			local isLend, value = matcher:compare(date, isPre)
			if value ~= date.month then
				refreshIdx = 3
			end
			date.month = value
			if isLend then
				date.year = date.year + 1 * factor
			end

			local dayMin = _ranges[4][1]
			local dayMax = getDayMax(date.month, date.year)
			if date.day >= dayMin and date.day <= dayMax then
				break
			end
		end
	else
		-- wday
		local w_matcher = self.matchers[_attribute[6]]
		local gap = w_matcher:getWdayGap(date.wday-1, isPre)
		if gap > 0 then
			refreshIdx = 3
			date.day = date.day + gap * factor
			-- dump(date, log .. " date 2")
		end

		-- month 
		local matcher = self.matchers[_attribute[5]]
		local isLend, value = matcher:compare(date, isPre)
		if value ~= date.month then
			refreshIdx = 3
			date.month = value
			if isLend then 
				date.year = date.year + 1 * factor
			end

			-- day
			local dayMin = _ranges[4][1]
			local dayMax = getDayMax(date.month, date.year)
			date.day = isPre and dayMax or dayMin
			date = os.date("*t" , os.time(date))
			-- dump(date, log .. " date 3")

			local new_gap = w_matcher:getWdayGap(date.wday-1, isPre)
			date.day = date.day + new_gap * factor
		end
	end

	if refreshIdx > 0 then
		for i=1,refreshIdx do
			local key = _attribute[i]
			date[key] = isPre and self.matchers[key]:getMax() or self.matchers[key]:getMin()
		end
		-- dump(date, log .. " date 4")
	end

	-- dump(date, log .. " date end")

	local newTime = os.time(date)
	if newTime == nowTime and not isLast then
		newTime = newTime + 1 * factor
		local nextTime, duration = self:next(newTime, isPre, true)
		duration = duration + 1 * factor
		-- print(log .. " nextTime = " .. nextTime)
		-- print(log .. " duration = " .. duration)
		-- dump(os.date("*t", newTime), log .. " newDate")
		return nextTime, duration
	else
		-- print(log .. " newTime = " .. newTime)
		-- print(log .. " duration = " .. (newTime - nowTime))
		-- dump(os.date("*t", newTime), log .. " newDate")
		return newTime, newTime - nowTime
	end
end

function Crontab:previous(nowTime)
	return self:next(nowTime, true)
end

function Crontab:getDayMaxPreMonth(year, month)
	return getDayMax(month, year)
end

return Crontab